/**
 * 
 */
package qy.jalgotrade.technical;

import java.time.ZonedDateTime;
import java.util.Map;

import qy.jalgotrade.dataseries.SequenceDataSeries;
import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.technical.ma.EMA;

/**
 * <pre>
 * Moving Average Convergence-Divergence indicator as described in
 * http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_average_convergence_divergence_macd.
 * </pre>
 * <pre>
 * DIFF: {@link SequenceDataSeries this};
 * DEA: {@link #getSignal()};
 * MACD: {@link #getHistogram()}
 * </pre>
 * 
 * @author c-geo
 *
 */
public class MACD extends SequenceDataSeries<Double> implements EventHandler {

	private int __fastEMASkip;

	private EMA.EMAEventWindow __fastEMAWindow;

	private EMA.EMAEventWindow __slowEMAWindow;

	private EMA.EMAEventWindow __signalEMAWindow;

	private SequenceDataSeries<Double> __signal;

	private SequenceDataSeries<Double> __histogram;

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param fastEMA    The number of values to use to calculate the fast EMA.
	 * @param slowEMA    The number of values to use to calculate the slow EMA.
	 * @param signalEMA  The number of values to use to calculate the signal EMA.
	 * @throws Exception
	 */
	public MACD(SequenceDataSeries<Double> dataSeries, int fastEMA, int slowEMA, int signalEMA) throws Exception {

		this(dataSeries, fastEMA, slowEMA, signalEMA, 0);
	}

	/**
	 * 
	 * @param dataSeries The DataSeries instance being filtered.
	 * @param fastEMA    The number of values to use to calculate the fast EMA.
	 * @param slowEMA    The number of values to use to calculate the slow EMA.
	 * @param signalEMA  The number of values to use to calculate the signal EMA.
	 * @param maxLen     The maximum number of values to hold. Once a bounded length is full, when new
	 *                   items are added, a corresponding number of items are discarded from the
	 *                   opposite end. If None then dataseries.DEFAULT_MAX_LEN is used.
	 * @throws Exception
	 */
	public MACD(SequenceDataSeries<Double> dataSeries, int fastEMA, int slowEMA, int signalEMA, int maxLen)
	        throws Exception {

		super(maxLen);
		assert fastEMA > 0;
		assert slowEMA > 0;
		assert fastEMA < slowEMA;
		assert signalEMA > 0;

		/*
		 * We need to skip some values when calculating the fast EMA in order for both EMA
		 * to calculate their first values at the same time.
		 * I'M FORCING THIS BEHAVIOUR ONLY TO MAKE THIS FITLER MATCH TA-Lib MACD VALUES.
		 */
		__fastEMASkip = slowEMA - fastEMA;

		__fastEMAWindow = new EMA.EMAEventWindow(fastEMA);
		__slowEMAWindow = new EMA.EMAEventWindow(slowEMA);
		__signalEMAWindow = new EMA.EMAEventWindow(signalEMA);
		__signal = new SequenceDataSeries<>(maxLen, Double.class);
		__histogram = new SequenceDataSeries<>(maxLen, Double.class);
		dataSeries.getNewValueEvent().subscribe(this);
	}

	/* (non-Javadoc)
	 * @see qy.jalgotrade.event.EventHandler#handle(java.lang.String, java.util.Map)
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		@SuppressWarnings("unchecked")
		SequenceDataSeries<Double> dataSeries = (SequenceDataSeries<Double>) params.get("dataSeries");
		ZonedDateTime dateTime = (ZonedDateTime) params.get("dateTime");
		double value = (double) params.get("value");
		__onNewValue(dataSeries, dateTime, value);
	}

	/**
	 * 
	 * @param dataSeries
	 * @param dateTime
	 * @param value
	 * @throws Exception
	 */
	private void __onNewValue(SequenceDataSeries<Double> dataSeries, ZonedDateTime dateTime, double value)
	        throws Exception {

		double diff = Double.NaN;
		double macdValue = Double.NaN;
		double signalValue = Double.NaN;
		double histogramValue = Double.NaN;

		/*
		 * We need to skip some values when calculating the fast EMA in order for both EMA
		 * to calculate their first values at the same time.
		 * I'M FORCING THIS BEHAVIOUR ONLY TO MAKE THIS FITLER MATCH TA-Lib MACD VALUES.
		 */
		__slowEMAWindow.onNewValue(dateTime, value);
		if (__fastEMASkip > 0) {
			__fastEMASkip--;
		} else {
			__fastEMAWindow.onNewValue(dateTime, value);
			if (__fastEMAWindow.windowFull()) {
				diff = __fastEMAWindow.getValue() - __slowEMAWindow.getValue();
			}
		}

		/*
		 * Make the first MACD value available as soon as the first signal value is available.
		 * I'M FORCING THIS BEHAVIOUR ONLY TO MAKE THIS FITLER MATCH TA-Lib MACD VALUES.
		 */
		__signalEMAWindow.onNewValue(dateTime, diff);
		if (__signalEMAWindow.windowFull()) {
			macdValue = diff;
			signalValue = __signalEMAWindow.getValue();
			histogramValue = macdValue - signalValue;
		}

		appendWithDateTime(dateTime, macdValue); // macdValue: DIF
		__signal.appendWithDateTime(dateTime, signalValue); // signalValue: DEA
		__histogram.appendWithDateTime(dateTime, histogramValue); // histogramValue: DIF - DEA
	}

	/**
	 * Returns a :class:`pyalgotrade.dataseries.DataSeries` with the EMA over the MACD.
	 * 
	 * @return
	 */
	public SequenceDataSeries<Double> getSignal() {

		return __signal;
	}

	/**
	 * Returns a :class:`pyalgotrade.dataseries.DataSeries` with the histogram (the difference between
	 * the MACD and the Signal).
	 * 
	 * @return
	 */
	public SequenceDataSeries<Double> getHistogram() {

		return __histogram;
	}
}
